package dao

import (
	"context"
	"github.com/ecodeclub/ekit/sqlx"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"time"
)

const (
	asyncStatusWaiting = iota
	asyncStatusFailed
	asyncStatusSuccess
)

var ErrWaitingSMSNotFound = gorm.ErrRecordNotFound

type Retry struct {
	Id     int64 `gorm:"primaryKey,autoIncrement"`
	Config sqlx.JsonColumn[SmsConfig]
	// 重试次数
	RetryCnt int
	// 重试的最大次数
	RetryMax int
	Status   uint8
	Ctime    int64
	Utime    int64 `gorm:"index"`
}
type SmsConfig struct {
	Biz     string
	Args    []string
	Numbers []string
}
type GormRetryDAO struct {
	db *gorm.DB
}
type RetryDAO interface {
	Insert(ctx context.Context, r Retry) error
	GetWaitingSMS(ctx context.Context) (Retry, error)
	MakeSuccess(ctx context.Context, id int64) error
	MakeFail(ctx context.Context, id int64) error
}

func NewGormRetryDAO(db *gorm.DB) RetryDAO {
	return &GormRetryDAO{db: db}
}
func (g GormRetryDAO) Insert(ctx context.Context, r Retry) error {
	now := time.Now().UnixMilli()
	r.Ctime = now
	r.Utime = now
	return g.db.WithContext(ctx).Create(&r).Error
}
func (g GormRetryDAO) GetWaitingSMS(ctx context.Context) (Retry, error) {
	// 如果在高并发情况下,SELECT for UPDATE 对数据库的压力很大
	// 但是我们不是高并发，因为你部署N台机器，才有 N 个goroutine 来查询
	// 并发不过百，随便写
	var retry Retry
	err := g.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		// 为了避开一些偶发性的失败，我们只找 1 分钟前的异步短信发送，悲观锁
		err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("utime<? and status=?",
			time.Now().Add(-time.Minute).UnixMilli(), asyncStatusWaiting).First(&retry).Error
		// SELECT xx FROM xxx WHERE xx FOR UPDATE，锁住了
		if err != nil {
			return err
		}
		// 只要更新了更新时间，根据我们前面的规则，就不可能被别的节点抢占了
		return tx.Model(&Retry{}).Where("id=?", retry.Id).
			Updates(map[string]any{
				"utime":     time.Now(),
				"retry_cnt": gorm.Expr("retry_cnt + 1"),
			}).Error
	})
	return retry, err
}
func (g GormRetryDAO) MakeSuccess(ctx context.Context, id int64) error {
	now := time.Now().UnixMilli()
	return g.db.WithContext(ctx).Model(&Retry{}).Where("id=?", id).Updates(map[string]any{
		"utime":  now,
		"status": asyncStatusSuccess,
	}).Error
}
func (g GormRetryDAO) MakeFail(ctx context.Context, id int64) error {
	now := time.Now().UnixMilli()
	return g.db.WithContext(ctx).Model(&Retry{}).Where("id=?", id).Updates(map[string]any{
		"utime":  now,
		"status": asyncStatusFailed,
	}).Error
}
func (Retry) TableName() string {
	return "retry"
}
